Skip to content

[육선우] sprint6#125

Open
ryuk6238-dot wants to merge 39 commits intocodeit-bootcamp-spring:육선우from
ryuk6238-dot:육선우sprint6

Hidden character warning

The head ref may contain hidden characters: "\uc721\uc120\uc6b0sprint6"
Open

[육선우] sprint6#125
ryuk6238-dot wants to merge 39 commits intocodeit-bootcamp-spring:육선우from
ryuk6238-dot:육선우sprint6

Conversation

@ryuk6238-dot
Copy link
Copy Markdown
Collaborator

요구사항

기본

  • 기본 항목 1
  • 기본 항목 2

심화

  • 심화 항목 1
  • 심화 항목 2

주요 변경사항

스크린샷

image

멘토에게

  • 셀프 코드 리뷰를 통해 질문 이어가겠습니다.

@ryuk6238-dot ryuk6238-dot added the 순한맛🐑 마음이 많이 여립니다.. label Mar 11, 2026
Copy link
Copy Markdown

@joonfluence joonfluence left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

전체 요약

Discord 클론 앱(Discodeit)의 Spring Boot + JPA 백엔드와 React + TypeScript 프론트엔드를 구현한 PR입니다. 전반적으로 잘 구조화되어 있으나, 평문 비밀번호 비교, N+1 쿼리, 불필요 파일 커밋 등 보안 및 성능 이슈가 존재합니다.

라인 코멘트로 달 수 없는 항목

[P3] .gitignore - 불필요 파일 커밋
.DS_Store, .discodeit/*.ser, data/storage/*, .postman/config.json, out/production/ 등 바이너리/시리얼라이즈 파일, macOS 시스템 파일, 로컬 스토리지 데이터, 빌드 결과물이 리포지토리에 포함되어 있습니다. .gitignore에 추가하고 git rm --cached로 제거해 주세요.

[P4] channelStore.ts - 공격적인 폴링 주기
채널 목록을 3초마다 폴링하는 것은 상당히 공격적입니다. 채널 목록은 자주 변경되지 않으므로 10-30초 간격이면 충분합니다. 장기적으로는 WebSocket 도입을 권장합니다.

.orElseThrow(
() -> new NoSuchElementException("User with username " + username + " not found"));

if (!user.getPassword().equals(password)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 🔒 평문 비밀번호 비교 — 보안 취약점

비밀번호를 평문으로 비교하고 있습니다. 회원가입 시에도 평문 저장(User 생성자에서 그대로 this.password = password), 로그인 시에도 equals()로 비교합니다.

반드시 BCrypt 등 해시 알고리즘을 사용해야 합니다.

Suggested change
if (!user.getPassword().equals(password)) {
if (!passwordEncoder.matches(password, user.getPassword())) {

회원가입(BasicUserService.create)에서도 passwordEncoder.encode()로 해싱 후 저장해야 합니다.

spring:
datasource:
url: jdbc:postgresql://localhost:5432/discodeit
username: discodeit_user
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] 🔒 DB 자격 증명 하드코딩

DB 자격 증명이 소스코드에 하드코딩되어 있습니다. 환경 변수나 Spring Profiles, Vault 등을 통해 외부화하세요.

Suggested change
username: discodeit_user
username: ${DB_USERNAME}
password: ${DB_PASSWORD}

@Transactional
public ChannelDto update(UUID channelId, PublicChannelUpdateRequest request) {
Channel channel = channelRepository.findById(channelId)
.orElseThrow(() -> new NoSuchElementException("Channel not found"));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 🐛 userId 파라미터 무시 — 보안 이슈

findAllByUserId(UUID userId) 메서드인데 userId 파라미터를 완전히 무시하고 channelRepository.findAll()모든 채널을 반환합니다. Private 채널까지 모든 사용자에게 노출되는 보안 이슈입니다.

userId에 해당하는 채널만 필터링하는 로직이 필요합니다:

  • PUBLIC 채널 전체 + 해당 userId의 ReadStatus가 있는 PRIVATE 채널만 반환

public ChannelDto toDto(Channel entity) {
if (entity == null) {
return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] ⚡ 심각한 N+1 쿼리 문제

채널 목록 조회 시 채널마다:

  1. messageRepository.findAllByChannelId() — 모든 메시지를 조회해 마지막 메시지 시간 계산
  2. readStatusRepository.findAllByChannelId() — 모든 ReadStatus 조회
  3. userRepository.findById() — 참여자별 개별 User 조회

채널 수가 늘면 성능이 급격히 저하됩니다.

개선안:

  • lastMessageAt: findTopByChannelIdOrderByCreatedAtDesc() 같은 단일 쿼리 사용
  • participants: @EntityGraph나 JOIN FETCH로 한 번에 조회
  • Mapper에서 Repository를 직접 호출하는 것 자체가 안티패턴입니다. Service 레이어에서 필요한 데이터를 미리 조회하여 Mapper에 전달하는 구조를 권장합니다.



@Mapping(source = "profile", target = "profile")
@Mapping(target = "online", constant = "false")
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 🐛 온라인 상태 항상 false로 하드코딩

UserStatus.isOnline() 로직이 구현되어 있지만, Mapper에서 constant = "false"로 하드코딩하여 모든 사용자가 항상 오프라인으로 표시됩니다.

MapStruct의 @Mapping(expression = ...)이나 커스텀 매핑 메서드를 사용하여 실제 UserStatus.isOnline() 결과를 반영해야 합니다.

public class Channel extends BaseEntity {

@Id
@GeneratedValue(strategy = GenerationType.UUID)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 🐛 BaseEntity와 중복된 @id 선언 + 타입 불일치

  1. BaseEntity를 상속하면서 @Id UUID id를 재선언하여 중복 매핑 문제가 발생합니다. BaseEntity에 이미 @Id가 정의되어 있으므로 제거하세요.
  2. createdAtLocalDateTime, updatedAtInstant로 타입이 불일치합니다. 일관되게 Instant를 사용하세요.
  3. @Setter가 Entity 클래스 레벨에 붙어 있어 캡슐화가 깨집니다. 필요한 필드만 setter를 제공하거나 도메인 메서드로 변경하세요.

ReadStatus 엔티티에서도 동일한 id/createdAt/updatedAt 재선언 문제가 있습니다.



@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", insertable = false, updatable = false)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 같은 컬럼에 대한 이중 매핑

user_id 컬럼을 @ManyToOne User user (insertable=false, updatable=false)와 @Column UUID userId 두 필드에 매핑하고 있습니다. 데이터 불일치 위험이 있고 JPA 설계로서 혼란스럽습니다.

User 관계만 유지하고 userIduser.getId()로 접근하는 것이 바람직합니다. 또는 반대로 userId만 유지하고 필요 시 조회하는 방식을 선택하세요.


@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleException(IllegalArgumentException e) {
e.printStackTrace();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] e.printStackTrace() 대신 Logger 사용 필요

e.printStackTrace()는 stdout으로 출력되어 로그 수집 시스템에서 누락됩니다. SLF4J Logger를 사용하세요.

Suggested change
e.printStackTrace();
log.warn("Bad request: {}", e.getMessage(), e);

또한 ErrorResponse DTO가 정의되어 있지만 사용하지 않고 plain String을 반환하고 있습니다. 일관된 에러 응답 형식을 위해 ErrorResponse를 활용하세요.

try {
this.updateLastActiveAt(userId);
} catch (Exception e) {
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] 조회 메서드의 부수효과(side effect) + 빈 catch 블록

find() (단순 조회)에서 updateLastActiveAt()을 호출하는 것은 예측하기 어려운 부수효과입니다. 또한 빈 catch 블록으로 모든 예외를 삼키고 있어 디버깅이 불가능합니다.

lastActiveAt 갱신은 별도 API 엔드포인트나 이벤트로 분리하세요.

open-in-view: false
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P4] ddl-auto: update는 프로덕션에서 위험

ddl-auto: update는 프로덕션에서 예기치 않은 스키마 변경을 유발할 수 있습니다. 개발 환경 전용임을 명시하거나, 프로필별로 분리하세요. Flyway/Liquibase 같은 마이그레이션 도구 도입도 고려해 보세요.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

순한맛🐑 마음이 많이 여립니다..

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants